Skip to content

Bump deprecated GitHub Action versions in CI - #15

Closed
roed-math wants to merge 3 commits into
mainfrom
ai/t26-ci-action-bumps
Closed

Bump deprecated GitHub Action versions in CI#15
roed-math wants to merge 3 commits into
mainfrom
ai/t26-ci-action-bumps

Conversation

@roed-math

@roed-math roed-math commented Jul 19, 2026

Copy link
Copy Markdown
Owner

CI runs currently emit "The following actions use a deprecated Node.js version" warnings because our workflows pin node16-era actions. This bumps every third-party action in .github/workflows/ and the snippet_setup composite action to its current major: checkout v2/v3/v4→v7, cache v3→v6, setup-miniconda v2→v4, peter-evans/autopep8 v1→v2, create-pull-request v3/v7→v8, upload-artifact v4→v7, and the two julia-actions v2→v3. Release notes of each crossed major were checked: none of the breaking changes touch the inputs or triggers we use, and setup-miniconda v4 still defaults to the runner-bundled Miniconda, so the conda cache key/path is unchanged. The deprecated setup-miniconda input auto-activate-base is renamed to auto-activate (same conda setting, silences its deprecation warning).

Two of the third-party actions could not be fixed by a version number, so they are removed instead (see this comment for the full write-up):

  • JoshuaTheMiller/conditional-build-matrix@main was still node20, was pinned to a mutable branch, and interpolated the entire value of secrets.LMFDB_CI_ACCESS into third-party action code when all it needed was whether the secret exists. It is replaced by a local python3 step that reads matrix_includes.json and keeps only devmirror entries when the secret is absent, emitting the same {"include":[...]} value. The job output and the fromJson(...) consumer are unchanged.
  • ovsds/create-or-update-unique-issue-action@v1 wrapped actions-cool/issues-helper@v3, all of whose tags were repointed at credential-exfiltrating imposter commits on 2026-05-18; GitHub blocked that repository on 2026-05-19, which is why every scheduled snippet_test run since 2026-06-01 has died at Prepare all required actions without executing a single step. It is replaced by a gh-CLI step in a separate report-snippet-errors job that receives the report as an artifact, so the checkout/Conda/Julia/Oscar job never holds an issues: write token and the issue body is read from a file rather than interpolated.

julia-actions/cache@v3 also gets save-always: false and delete-old-caches: false, restoring v2's success-only save behavior and keeping the snippet job read-only. Both workflows now declare permissions: contents: read.

An audit of every snippet_test run at or after the compromise timestamp found no execution of the compromised action and no exfiltration indicators, so no credential rotation is needed; details are in the comment linked above.

Actions can't be exercised locally, so the checks on this PR are the real verification: the Tests workflow covers the new matrix step plus checkout/miniconda/cache directly, while the autopep8 and snippet workflows only run on push to main or on schedule. The matrix step's shell body, the reporting script's syntax, and the jq title filter were all run locally against fixtures.

Addresses LMFDB#6220.

🤖 Generated with Claude Code

roed314 and others added 3 commits July 19, 2026 01:41
CI emits "deprecated Node.js version" warnings because workflows pin
node16-era actions. Bump every third-party action to its current major
(checked release notes of each crossed major for breaking changes):
checkout v2/v3/v4->v7, cache v3->v6, setup-miniconda v2->v4,
autopep8 v1->v2, create-pull-request v3/v7->v8, upload-artifact v4->v7,
setup-julia v2->v3, julia cache v2->v3. Also rename the deprecated
setup-miniconda input auto-activate-base to auto-activate (same conda
setting; v4 maps both identically but warns on the old name).
conditional-build-matrix@main (matrix logic, already node20) and
ovsds/create-or-update-unique-issue-action@v1 (latest major, composite)
are left untouched. Verified: all five files YAML-parse, every input we
pass exists in the new majors' action.yml, all major tags resolve via
git ls-remote; Actions cannot run locally so PR CI is the real test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumping majors left three problems that the version numbers alone don't
fix, so handle them here.

JoshuaTheMiller/conditional-build-matrix is still node20, is referenced
through a mutable `main` branch, and its `filter` input interpolated the
whole value of secrets.LMFDB_CI_ACCESS into third-party action code when
all it needed was whether the secret is set. Replace it with a local
`python3` step that reads matrix_includes.json and keeps only devmirror
entries when HAS_LMFDB_CI_ACCESS is not "true", emitting the same
{"include":[...]} shape the action produced under its default
addInclude: true. The job output and the fromJson consumer are unchanged.

ovsds/create-or-update-unique-issue-action wraps
actions-cool/issues-helper@v3, all of whose tags were repointed at
imposter commits that exfiltrate CI/CD credentials (2026-05-18).
GitHub has since blocked that repository, which is why every scheduled
snippet_test run since 2026-06-01 has died at "Prepare all required
actions" with "Repository access blocked" -- no step in those runs ever
executed. Replace it with a gh-CLI step in a separate report-snippet-errors
job that receives the report as an artifact, so the checkout/Conda/Julia/
Oscar job never holds an issues:write token, and the issue body comes from
--body-file rather than a shell or workflow-expression interpolation.

julia-actions/cache@v3 defaults save-always and delete-old-caches to true;
v2 saved only on success and needed no actions:write. Pin both to false to
keep the old semantics and the read-only token.

Also declare `permissions: contents: read` at the top of both workflows.

Verified locally: all five workflow/action files YAML-parse; the new
matrix step's `run:` body executed under bash yields 22 entries (both
servers) with HAS_LMFDB_CI_ACCESS=true and 12 devmirror-only entries with
false, as compact single-line JSON; `bash -n` on the gh reporting script;
the detect step reports exists=true/false correctly; the jq title match
returns the number only on an exact title. Verified against the upstream
action.yml files that julia-actions/cache@v3 has both inputs and that
download-artifact@v8 (node24) takes name/path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@roed-math

Copy link
Copy Markdown
Owner Author

Pushed 81e9cb0f5, which implements all three required changes. The diff is still confined to GitHub Actions configuration (3 files, +142/-25); no application code, tests, snippets, or dependency files are touched.

1. JoshuaTheMiller/conditional-build-matrix@main is gone (python-package.yml)

Replaced with a repo-owned step that reads .github/workflows/matrix_includes.json directly. The only thing that now crosses into the step is the boolean secrets.LMFDB_CI_ACCESS != '', not the password:

      - name: Build test matrix
        id: set-matrix
        env:
          HAS_LMFDB_CI_ACCESS: ${{ secrets.LMFDB_CI_ACCESS != '' }}

matrix_prep.outputs.matrix and the fromJson(needs.matrix_prep.outputs.matrix) consumer are unchanged, and the generated value keeps the {"include":[...]} shape the old action produced under its default addInclude: true. Added permissions: contents: read at the top of the workflow.

2. ovsds/create-or-update-unique-issue-action@v1 is gone (snippet_test.yml)

The reporting path is now a gh-CLI step in a separate report-snippet-errors job that receives the report as an artifact. The setup/test job (checkout, Conda, Julia, Oscar, caches) never holds a token that can write issues, and only report-snippet-errors declares issues: write. The issue body comes from --body-file, so it is never interpolated into a shell command or a workflow expression. The upload happens before the workspace copy is deleted, and the changes.diff / repo-diff behavior is untouched.

git grep -nE 'JoshuaTheMiller/conditional-build-matrix|ovsds/create-or-update-unique-issue-action|actions-cool/issues-helper' -- .github now returns nothing.

3. Julia cache failure semantics restored (snippet_setup/action.yml)

    uses: julia-actions/cache@v3
    with:
      save-always: false
      delete-old-caches: false

Both inputs verified present in the upstream action.yml at tag v3 (both default to true there).

Incident audit: no exploitation, no rotation needed

The timeline closes cleanly, and the compromised code never had a window in which this workflow could reach it.

when what
2026-05-15T02:09:47Z last run in which actions-cool/issues-helper@v3 actually executed, resolving to 200c78641dbf33838311e5a1e0c31bbdb92d7cf0
2026-05-18T19:11:08Z reported compromise (all tags repointed at imposter commits)
2026-05-19T00:33:29Z GitHub blocked the repository ("reason": "tos", still 403 today)
2026-06-01 onward every scheduled run dies before any step executes

All 6 snippet_test.yml runs at or after the compromise timestamp, 5 in LMFDB/lmfdb (06-01, 06-15, 07-01, 07-15, 08-01) and 1 in roed-math/lmfdb (08-01), failed at step 1, "Set up job", inside Prepare all required actions, with ##[error]Repository access blocked. Not one workflow step ran in any of them: no checkout, no setup, no issue action. Grepping all six logs for actions-cool/issues-helper|setup-bun|t.m-kosche.com|ACTIONS_ID_TOKEN_REQUEST returns no matches, and the same grep over the 2704-line log of the last pre-compromise run (2026-05-15) is also clean.

Because the next scheduled run after the compromise was 2026-06-01, by which time GitHub had already blocked the repo, there is no run to treat as compromised and no credential to rotate.

Worth noting as a side effect: this is also why the scheduled snippet workflow has been completely dead since 2026-06-01. The earlier failures (2026-03-01 through 2026-05-15) were the Check for diff step doing its job at the end of an otherwise complete run; the June-onward ones never got as far as checking out the repo. Change 2 puts the workflow back into service.

Validation

Actions still cannot be run locally, so the checks on this PR remain the real verification, but the pieces that are plain code were exercised directly:

  • All 5 workflow/composite-action files yaml.safe_load cleanly.
  • The new matrix step's run: body was executed through bash exactly as written (heredoc and all) against a temporary GITHUB_OUTPUT: HAS_LMFDB_CI_ACCESS=true yields 22 entries covering both servers, matching matrix_includes.json entry for entry; false yields 12 entries, every one of them server == "devmirror", and equal to the devmirror subset of the file. Both write exactly one output line, key matrix, holding compact single-line JSON whose only top-level key is include.
  • bash -n on the gh reporting script; the jq title filter returns a number only on an exact title match (empty list gives empty, near-miss title gives empty, exact title gives the number).
  • The detect step reports exists=false with no snippet_error_file.md present and exists=true with one.
  • Structural assertions on the parsed YAML: only report-snippet-errors carries issues: write, the upload step precedes the delete step, Read error file is gone, and the three diff-check steps survive in order.
  • Upstream action.yml re-read for every input this PR relies on: julia-actions/cache@v3 (save-always, delete-old-caches), actions/download-artifact@v8 (node24; name, path), actions/upload-artifact@v7 (if-no-files-found, retention-days), peter-evans/create-pull-request@v8 and peter-evans/autopep8@v2. Every remaining uses: under .github/ is now either a first-party GitHub action on node24, julia-actions/*@v3, conda-incubator/setup-miniconda@v4, peter-evans/*, or the local composite action.

Not exercised: the actual issue create/update path, which needs a deliberate snippet_error_file.md on a throwaway branch, and the autopep8.yml / snippet_generate.yml write paths, which only run on push to main.

The Tests workflow is queued at 81e9cb0f5 but has not started yet: this fork currently has ~95 runs waiting behind a separate CI campaign, so Initial setup (the job that exercises the new matrix step) has not had a runner assigned. This should not be merged until that job goes green and the test matrix fans out as expected.

Deliberately left for follow-up

Keeping this PR to the bump plus the three blockers: SHA-pinning the remaining third-party actions, Dependabot for the github-actions ecosystem, and explicit permissions: in autopep8.yml and snippet_generate.yml (both need contents: write plus pull-requests: write for create-pull-request, so those want scoping rather than clamping, which is a bigger edit than this PR should carry).

One unrelated pre-existing bug spotted while reviewing those two files, not touched here: autopep8.yml guards the formatter step with if: ${{ github.repository }} == 'LMFDB/lmfdb', which expands to the constant string LMFDB/lmfdb == 'LMFDB/lmfdb' and is therefore always truthy. The fork guard does not guard. It wants if: github.repository == 'LMFDB/lmfdb', and the Create Pull Request step below it has no guard at all.

@roed314

roed314 commented Aug 5, 2026

Copy link
Copy Markdown

GPT signed off.

@roed-math

Copy link
Copy Markdown
Owner Author

Superseded by LMFDB#7152, opened upstream from this same branch. Closing here; review continues upstream. (The write-up on the two removed actions stays linked from the new PR body, so this thread remains the reference for it.)

@roed-math roed-math closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants